from django import forms
from django.core.urlresolvers import reverse
from django.http import HttpResponse
from django.http import HttpResponseRedirect
from django.forms import ModelForm
from django.forms.models import modelformset_factory
from django.shortcuts import render_to_response, get_object_or_404
from django.template import RequestContext, loader
from inventory.models import Material, Component
from inventory.rendering import Renderer
from inventory.import_export import handle_import, handle_export

import csv
import sets

# TODO Features:
# - Clone an accord so I can branch it without affecting other formulas.
#   copy the material, rename, save. copy all Components, replacing accord id.
#   redirect to new material.
# - highlight query in search results
# - Button to 'Add' component (hide the extra until clicked, or reload
#   page with param extra=on, but that's cheesier)
# - Component form element should be a textbox with autocomplete,
#   not a select dropdown.
# - Javascript confirm delete.
# - Multi-user support!  Then ask perfumemaking for beta testers.
# - Make it pretty

# TODO Bugs:
# - in material view, check 'delete' on the extra component row -> validation
#   error on the formset.
# - Better Exception handling throughout.

class MaterialForm(ModelForm):
  class Meta:
    model = Material

class ComponentForm(ModelForm):
  class Meta:
    model = Component

class RenderForm(forms.Form):
  requested_total = forms.FloatField(min_value=0.0)
  sort_key = forms.CharField(widget=forms.HiddenInput)

class OneLineMaterialForm(MaterialForm):
  notes = forms.CharField()
  class Meta:
    model = Material
    fields = ('name', 'notes')

def render(request, mat_id, requested_total=100, sort_key='a'):
  message = ''
  if len(request.POST):
    form = RenderForm(request.POST)
    if form.is_valid():
      requested_total = form.cleaned_data['requested_total']
      sort_key = form.cleaned_data['sort_key']
  mat = get_object_or_404(Material, pk=mat_id)
  renderer = Renderer(mat, requested_total, sort_key)
  render_form = RenderForm(initial={'requested_total' : requested_total,
                                    'sort_key' : sort_key })
  return render_to_response('sniffbook/rendered.html',
      { 'mat' : mat,
        'renderer' : renderer,
        'render_form' : render_form,
        'error_message' : message, },
      context_instance=RequestContext(request))

def accordlist(request, show_all=True, sort_field='name', message=''):
  if len(request.POST):
    try:
      form = MaterialForm(request.POST, prefix='mat')
      if form.is_valid():
        form.save()
        # FIXME log
        #print 'saved ', form.instance
      else:
        message = 'Invalid data: %s' % form.errors()
    except Exception, e:
      # TODO bad, don't catch generic exceptions.
      message = 'Error!'
    else:
      message = 'New material added.'
      url = reverse('inventory.views.material',
                    args=[form.instance.id])
      # Hack because I can't figure out how to control which URL reverse() maps
      # to (seems to prefer to most specific/long url).
      url = url.replace('/delete/', '')
      # FIXME log
      #print 'redirecting to ', url
      return HttpResponseRedirect(url)
  if show_all:
    unsorted = Material.objects.all().order_by(sort_field)
  else:
    unsorted = Material.objects.filter(is_accord=True).order_by(sort_field)
  if sort_field == 'bottle_num':
    mats = unsorted.filter(bottle_num__isnull=False).order_by(sort_field)
  else:
    mats = unsorted.order_by(sort_field)
  new_mat_form = OneLineMaterialForm(prefix='mat')
  return render_to_response('sniffbook/list.html',
      { 'accords' : mats,
        'new_mat_form' : new_mat_form,
        'error_message' : message, },
      context_instance=RequestContext(request))

def search(request):
  results = None
  message = ''
  if request.method == 'GET':
    query = request.GET['q']
    # appengine backend doesn't support OR queries, or __contains.  So switching
    # to nonrel-search.  The downside is that it does tokenization and I
    # liked having partial word search.  Also when I added the index, I had to
    # re-save every entry so that it would get indexed.
    # TODO: max results, pagination.
    #mats = Material.objects
    #results = (mats.filter(name__icontains=query) |
    #           mats.filter(notes__icontains=query))
    results = Material.search_index.search(query)
    
  return render_to_response('sniffbook/search.html',
      { 'results' : results,
        'error_message' : message },
      context_instance=RequestContext(request))

def material(request, mat_id, delete_me=False):
  message = ''
  mat = get_object_or_404(Material, pk=mat_id)
  ComponentFormSet = modelformset_factory(Component, extra=1, can_delete=True)
  if delete_me:
    if mat.material_set.count():
      message = 'Can not delete.  This material is used in other accords: '
      names = []
      for c in mat.material_set.all():
        names.append(c.accord.name)
      message += ','.join(names)
    else:
      # FIXME log
      # print 'Deleting ', mat
      message = 'Material deleted.'
      # FIXME: need to delete the back references too.. Get all components that
      # refer to this, and delete them.
      mat.delete()
      return HttpResponseRedirect(
          reverse('inventory.views.accordlist',
                  kwargs={ 'message' : message }))
  elif len(request.POST):
    try:
      form = MaterialForm(request.POST, instance=mat, prefix='mat')
      comp_formset = ComponentFormSet(request.POST,
                                      prefix='comps',
                                      queryset=mat.component_set.all())
      if form.is_valid() and comp_formset.is_valid():
        num_comps = (len(comp_formset.forms) -
                     len(comp_formset.deleted_forms))
        if not comp_formset.extra_forms[0].cleaned_data:
          # discount the empty extra form
          num_comps -= 1
        form.save()

        # Since we don't show the Accord field in the form (which would be
        # redundant since they all should point to this material), we need to
        # set it here.
        instances = comp_formset.save(commit=False)
        for comp in instances:
          comp.accord = mat
          comp.save()

        # Didn't work to modify form.instance.is_accord, so trying this:
        should_be_accord = (num_comps > 0)
        if mat.is_accord != should_be_accord:
          mat = Material.objects.get(pk=mat_id)
          mat.is_accord = should_be_accord
          mat.save()
      else:
        message = 'Not valid data: %s, %s' % (form.errors(),
                                              comp_formset.errors())
    except (KeyError, Material.DoesNotExist):
      message = 'Bad something'
    except ValueError:
      # Or should I check form.is_valid() explicitly?
      message = 'Data did not validate'
    else:
      message += 'Your changes were saved.'

  form = MaterialForm(instance=mat, prefix='mat')

  comp_formset = ComponentFormSet(queryset=mat.component_set.all(),
                                  prefix='comps')

  return render_to_response('sniffbook/material.html',
      { 'mat' : mat,
        'error_message' : message,
        'form' : form,
        'comp_formset' : comp_formset, },
      context_instance=RequestContext(request))


class ImportFileForm(forms.Form):
  file = forms.FileField()


# Only <1M files are supported now (in-memory file limit).  When blobstore
# FileField support happens, should take bigger files.  but 1M should be enough.
# TODO: Progress update while it's working.
def import_export(request):
  message = ''
  if len(request.POST):
    form = ImportFileForm(request.POST, request.FILES)
    if form.is_valid():
      # FIXME this is an UploadedFile object, not data. Read data by testing
      # multiple_chunks() (not supported yet) and then chunks()
      (success, message) = handle_import(request.FILES['file'])
      if success:
        return HttpResponseRedirect(
          reverse('inventory.views.accordlist',
                  kwargs={ 'message' : message }))
      else:
        return HttpResponseRedirect(
          reverse('inventory.views.import_export',
                  kwargs={ 'message' : message }))
    else:
      message = 'Form invalid: %s' % form.errors
  form = ImportFileForm()
  return render_to_response('sniffbook/import.html',
      { 'form': form,
        'error_message' : message, },
      context_instance=RequestContext(request))


def export(request, export_type):
  message = ''
  filename = '%s.csv' % export_type
  response = HttpResponse()
  response['mimetype'] = 'text/csv'
  # FIXME: file comes with an .html extension. what up?
  response['Content-Disposition'] = 'attachment; filename=%s' % filename
  (success, message) = handle_export(export_type, response)
  if success:
    return response
  return HttpResponseRedirect(
    reverse('inventory.views.import_export',
            kwargs={ 'message' : message }))
